﻿/*****************************************************************************\
 *             University of Illinois/NCSA Open Source License
 *
 * 
 *      Copyright © 2012,2013, The Board of Trustees of the University of Illinois. All rights reserved.
 *
 *      Developed by: Rakesh Bobba, Derek Dagit, Erich Heine, Denis Kholine,
 *                    Klara Nahrstedt, David Rodgers, William H. Sanders, Tim Yardley
 *
 *      Information Trust Institute/Coordinated Science Laboratory
 * 
 *      University of Illinois
 *      
 *      The Board of Trustees of the University of Illinois
 *      109 Coble Hall
 *      801 South Wright Street
 *      Champaign, IL 61820-6242
 *
 *      http://www.iti.illinois.edu / http://csl.illinois.edu
 * 
 *      Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 
 *      associated documentation files (the “Software”), to deal with the Software without restriction, 
 *      including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *      and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
 *      subject to the following conditions:
 *
 *      Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *      following disclaimers.
 *      
 *      Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
 *      the following disclaimers in the documentation and/or other materials provided with the distribution.
 *
 *      Neither the names of Information Trust institute, Coordinated Science Laboratory, University of Illinois,
 *      The Board of Trustees of the University of Illinois nor the names of its contributors may be used to 
 *      endorse or promote products derived from this Software without specific prior written permission.
 *      
 *      THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
 *      LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN
 *      NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 *      WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 *      SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.
 *      
 \*****************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GSF;
using SIEGate.Core.Adapters;
using SIEGate.Core.Legacy;
using SIEGate.Core.Measurements;
using SIEGate.Core.Measurements.Values;

namespace SIEGate.Core.Stats
{
    /// <summary>
    /// Emit a controlled set of StatInsts to the data bus.
    /// </summary>
    public class StatMapper : InputAdapter
    {
        // Strategy pattern.  We need a way to capture the current value of a
        // stat in order to push it to the data bus with a MeasurementValue.
        /// <summary>
        /// Creates Measurements regarding stats from IDuplicableStats.
        /// </summary>
        public abstract class StatMeasurementCreator
        {
            protected readonly IDuplicableStat Stat;
            public Guid StreamId { get; private set; }

            protected StatMeasurementCreator(IDuplicableStat stat, Guid streamId)
            {
                Stat = stat;
                StreamId = streamId;
            }

            public abstract dynamic CreateMeasurement();
        }

        public class AdapterInfo : AdapterInfoBase
        {
            private struct DataContainer
            {
                public uint StatSourceId;
                public string StatName;
                public TypeOfStat StatType;
            }

            private Dictionary<Guid, DataContainer> m_mapGuidsToDataContainers = new Dictionary<Guid, DataContainer>();

            private static readonly string[] TableNamesWithAssociatedStats = new string[] { "InputAdapters", "ActionAdapters", "OutputAdapters", "NetworkCommunicators" };

            private static readonly Dictionary<string, string> MapSourceAcronymToSourceCategory = new Dictionary<string, string>();

            static AdapterInfo()
            {
                MapSourceAcronymToSourceCategory.Add("OS",  "OutputStream");
                MapSourceAcronymToSourceCategory.Add("IS",  "InputStream");
                MapSourceAcronymToSourceCategory.Add("PMU", "Device");
                MapSourceAcronymToSourceCategory.Add("SUB", "Subscriber");
                MapSourceAcronymToSourceCategory.Add("PUB", "Publisher");
                MapSourceAcronymToSourceCategory.Add("SYSTEM",  "System");
            }

            public override ISet<Guid> OutgoingStreamIDs
            {
                get
                {
                    return new HashSet<Guid>(m_mapGuidsToDataContainers.Keys);
                }
                set
                {
                    throw new InvalidOperationException("Cannot set OutgoingStreamIDs for StatMapper.AdapterInfo");
                }
            }

            public override void ExtractConfiguration(DataSet configurationTables)
            {
                base.ExtractConfiguration(configurationTables);
                DataRow[] rows = configurationTables.Tables["ActiveMeasurements"].Select(string.Format("ID LIKE '{0}:*'", "STAT"));

                foreach (DataRow row in rows)
                {
                    DataContainer dataContainer = new DataContainer();

                    // "{ADAPTER_ACRONYM}!{CATEGORY}-{STATINDEX}"
                    // e.g., "FOO!IS-1"
                    string signalReference = (string)row["SignalReference"];
                    
                    string[] splitString = signalReference.Split('!');

                    if (splitString.Length != 2)
                    {
                        LogDispatcher.E(StrLogSource, StrLogTag, "Failed to extract acronym and signal information from signal reference {0}", signalReference);
                        continue; // TODO: revisit, recover and do what we can
                    }

                    string[] splitCategoryStatIndex = splitString[1].Split('-');
                    string signalCategoryAcronym = splitCategoryStatIndex[0];
                    string signalIndex = splitCategoryStatIndex[1];

                    if (splitCategoryStatIndex.Length != 2)
                    {
                        LogDispatcher.E(StrLogSource, StrLogTag, "Failed to extract signal type and signal index from signal reference {0}", signalReference);
                        continue; // TODO: revisit, recover and do what we can
                    }

                    #region Get SystemStatSourceId
                    // Resolve ADAPTER_ACRONYM; look through ActionAdapters, InputAdapters, OutputAdapters, NetworkCommunicators tables.
                    // One of those will have the acronym.  Using that row, extract the ID.  There's SystemStatSourceId.
                    string signalAcronym = splitString[0];
                    foreach (string tableName in TableNamesWithAssociatedStats)
                    {
                        DataRow[] matchingRows =
                            configurationTables.Tables[tableName].Select(string.Format("AdapterName LIKE '{0}'",
                                                                                       signalAcronym));
                        // TODO: This should check for exactly 1, not nonzero.  If it finds more than 1 something went wrong.
                        if (matchingRows.Length > 0)
                        {
                            // I'm concerned because in Measurement this row is treated as a string, not a uint.
                            dataContainer.StatSourceId = Convert.ToUInt32(matchingRows[0]["ID"]);
                            break;
                        }
                    }
                    #endregion

                    // StatName gets extracted from Statistics based on whether {CATEGORY} was IS, OS, or whatever.
                    // System, Device, InputStream, OutputStream, Subscriber, Publisher
                    if (!MapSourceAcronymToSourceCategory.ContainsKey(signalCategoryAcronym))
                    {
                        LogDispatcher.E(StrLogSource, StrLogTag, "Cannot resolve category acronym {0}", signalCategoryAcronym);
                        continue; //TODO: revisit, for now make it work with what we can
                    }
                    
                    // Then look in the Statistic table for {STATINDEX}, that will give you StatName
                    DataRow[] signalIndexRows =
                        configurationTables.Tables["Statistics"].Select(
                            string.Format("Source = '{0}' AND SignalIndex = {1}",
                                          MapSourceAcronymToSourceCategory[
                                              signalCategoryAcronym],
                                          signalIndex.Substring(2)));
                    if (signalIndexRows.Length != 1)
                    {
                        LogDispatcher.E(StrLogSource, StrLogTag,
                                        "Did not get one row when attempting to retrieve signal index {0}.  Got {1} row(s).",
                                        signalIndex, signalIndexRows.Length);
                        continue; // TODO: revisit, for now, just ignore bad stats.
                    }
                    dataContainer.StatName = (string)(signalIndexRows.First()["Name"]);

                    // TODO: Check that Current is the only thing to use here.
                    dataContainer.StatType = TypeOfStat.Current;

                    m_mapGuidsToDataContainers.Add((Guid) row["SignalID"], dataContainer);
                }
            }

            /// <summary>
            /// Gets that statSourceId, statName, and StatType for a given Guid.
            /// </summary>
            /// <param name="guid">The Guid of the output stream.</param>
            /// <param name="statSourceId">The id associated with the object the statistic retrieved is about.</param>
            /// <param name="statName">The name of the statistic.</param>
            /// <param name="statType">What type of value to send to the data bus when data is retrieved.</param>
            public bool TryGetStreamInfo(Guid guid, out uint statSourceId, out string statName, out TypeOfStat statType)
            {
                if (m_mapGuidsToDataContainers.ContainsKey(guid))
                {
                    DataContainer dc = m_mapGuidsToDataContainers[guid];
                    statSourceId = dc.StatSourceId;
                    statName = dc.StatName;
                    statType = dc.StatType;
                    return true;
                }
                statSourceId = 0;
                statName = null;
                statType = TypeOfStat.Current;
                return false;
            }
        }

        protected Stats StatsSource
        {
            get;
            set;
        }

        // Stores the current value.  Will be copied to Measurement on Emit.
        // Key:  The Guid of the stream output to the data bus.
        // Value:  A StatMeasurementCreator that can create Measurements for that Guid from a StatInst.
        private readonly IDictionary<Guid, StatMeasurementCreator> _mStats = new Dictionary<Guid, StatMeasurementCreator>();
        private const string StrLogTag = "StatMapper";
        private const string StrLogSource = "StatMapper";

        //public new ISet<Guid> OutgoingStreamIDs
        //{
        //    get
        //    {
        //        // HACK: Instantiating new HashSets is very expensive.  See if we can change OutgoingStreamIDs to be a ICollection instead of an ISet.
        //        // Alternately, in Add/Remove store in a different set.
        //        // Alternately, have a structure that stores as both Set and Dictionary.
        //        HashSet<Guid> toReturn = new HashSet<Guid>();
        //        foreach (Guid guid in _mStats.Keys)
        //        {
        //            toReturn.Add(guid);
        //        }
        //        return toReturn;
        //    }
        //}

        public StatMapper(IAdapterInfo conf)
            : this(Stats.StatDispatcher, conf)
        {
        }

        private StatMapper(Stats statSource, IAdapterInfo conf)
            : base(conf)
        {
            StatsSource = statSource;
            foreach (Guid guid in conf.OutgoingStreamIDs)
            {
                AttemptAddMeasurementCreatorForGuid(guid);
            }
            Interval.Instance.StatOutputters += StatsReadyEvent;
        }

        private void AttemptAddMeasurementCreatorForGuid(Guid guid)
        {
            StatMeasurementCreator measurementCreator = GetMeasurementCreatorForGuid(guid);
            if (measurementCreator != null)
            {
                _mStats.Add(guid, measurementCreator);
            }
        }

        protected override void HandleMeasurement(IMeasurement m)
        {
            // No-op, there shouldn't be any calls to HandleMeasurement since
            // this mapper/adapter doesn't draw from the data bus.
        }

        /// <summary>
        /// Gather the measurements associated with this adapter into a data passing structure.
        /// </summary>
        /// <returns>A StatMeasurement with data at the time CreateMeasurement was called.</returns>
        protected Measurement CreateMeasurement(Guid guid)
        {
            if (!_mStats.ContainsKey(guid))
            {
                LogDispatcher.D(StrLogSource, StrLogTag, "Did not have an instantiated measurement creator for guid {0} when attempting to create a Measurement.", guid);
                // The source of the stat didn't exist when the StatMapper was instantiated, attempt to instantiate now
                AttemptAddMeasurementCreatorForGuid(guid);
                if (!_mStats.ContainsKey(guid))
                {
                    LogDispatcher.D(StrLogSource, StrLogTag,
                                    "Failed to instantiate a measurement creator for guid {0} when attempting to create a Measurement.",
                                    guid);
                }
            }
            return _mStats[guid].CreateMeasurement();
        }

        public void StatsReadyEvent(object sender, EventArgs e)
        {
            // We need it as an array for params' sake, so disabling CoVariantArrayConversion
            // TODO: ReSharper says this can be converted to a LINQ expression but I found that comparatively difficult to read.
            // If it's actually faster, we should change to that, otherwise don't apply the refactoring.
            List<Measurement> toEmit = new List<Measurement>();
            foreach (Guid guid in OutgoingStreamIDs)
            {
                Measurement measurement = _mStats[guid].CreateMeasurement();
                if (measurement != null)
                {
                    toEmit.Add(measurement);
                }
            }
// ReSharper disable CoVariantArrayConversion
            OnEmit(toEmit.ToArray());
// ReSharper restore CoVariantArrayConversion
        }

        /// <summary>
        /// Instantiates a MeasurementCreator for a given output stream Guid.
        /// </summary>
        /// <param name="guid">The Guid of the stream output by this 
        /// StatMapper for that statistic.</param>
        /// <returns>Null if the StatMeasurementCreator cannot be created.  
        /// The usual reason for this is that the StatInst or source of the 
        /// StatInst has not been created.</returns>
        private StatMeasurementCreator GetMeasurementCreatorForGuid(Guid guid)
        {
            uint statSourceId;
            string statName;
            TypeOfStat statType;

            ((AdapterInfo) Info).TryGetStreamInfo(guid, out statSourceId, out statName, out statType);
            
            StatInst statInst = Stats.StatDispatcher.GetStatistic(statSourceId, statName);
            return statInst == null ? null : statInst.GetMeasurementCreator(guid, statSourceId, statType);
        }
    }
}
